﻿using System;
using System.IO;
using System.Collections.Generic;
using System.Text;

using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;

using SharpSvn;

namespace SvnTools.Tasks.VersionManagement
{
	public class SvnSetExternals : Task
	{
		private ITaskItem[] m_externals = null;

		#region Properties

		public string TargetPath { get; set; }

		[Required]
		public ITaskItem[] Externals
		{
			set { m_externals = value; }
		}

		/// <summary>
		/// List of targets to include.  If provided, only these targets will be updated.
		/// If both IncludeTargets and ExcludeTargets are specified, ExcludeTargets is ignored.
		/// </summary>
		public string[] IncludeTargets { get; set; }

		/// <summary>
		/// List of targets to exclude.  If provided, these targets will left unchanged.
		/// If both IncludeTargets and ExcludeTargets are specified, ExcludeTargets is ignored.
		/// </summary>
		public string[] ExcludeTargets { get; set; }

		/// <summary>
		/// Set to true to cause a debugger break when the task is called.
		/// Default = false;
		/// </summary>
		public bool Debug { get; set; }

		/// <summary>
		/// Force externals to save in a backward-compatible format.
		/// Default = false;
		/// </summary>
		public bool UseCompatibleFormat { get; set; }

		#endregion

		public override bool Execute()
		{
			if (Debug) { System.Diagnostics.Debugger.Break(); }

			bool success = true;

			try
			{
				// If TargetPath isn't set, use the current build directory
				string target_path = TargetPath;
				if (string.IsNullOrEmpty(target_path))
				{
					target_path = Path.GetDirectoryName(BuildEngine.ProjectFileOfTaskNode);
				}

				string svn_externals;
				// Get the existing svn:externals value
				using (SvnClient client = new SvnClient())
				{
					if (!client.GetProperty(target_path, "svn:externals", out svn_externals))
					{
						Log.LogWarning("No svn:externals property present in target path: {0}", target_path);
					}
				}

				List<SvnExternalItem> new_ext_items = new List<SvnExternalItem>();
				SvnExternalItem[] old_ext_items = null;
				if (!SvnExternalItem.TryParse(svn_externals, out old_ext_items))
				{
					// Couldn't read old items, so start with a clean slate
					old_ext_items = new SvnExternalItem[0];
				}

				// Walk through the old items.  If it's included in the update,
				// use the new values; otherwise, use the old values.
				foreach (SvnExternalItem old_ext_item in old_ext_items)
				{
					if (ShouldUpdateExternal(old_ext_item.Target))
					{
						ITaskItem new_task_item = FindExternalTaskItem(old_ext_item.Target);
						if (new_task_item == null)
						{
							// Didn't find it in the new list.  Use the old one
							new_ext_items.Add(old_ext_item);
						}
						else
						{
							new_ext_items.Add(CreateExternal(new_task_item));
						}
					}
					else
					{
						// Not included in the update. Use the old value
						new_ext_items.Add(old_ext_item);
					}
				}

				// Walk through the new items that we haven't already addressed.  
				foreach (ITaskItem new_task_item in m_externals)
				{
					// try to find this external in the old list
					bool can_find_in_old = false;
					foreach (SvnExternalItem old_ext_item in old_ext_items)
					{
						if (old_ext_item.Target.Equals(new_task_item.ItemSpec, StringComparison.InvariantCultureIgnoreCase))
						{
							can_find_in_old = true;
							break;
						}
					}

					if (!can_find_in_old)
					{
						// we haven't addressed it yet.
						new_ext_items.Add(CreateExternal(new_task_item));
					}
				}

				// Now we have the list of new SvnExternalItems.   Turn them back into a string and set the property
				StringBuilder svn_externals_builder = new StringBuilder();
				foreach (SvnExternalItem ext_item in new_ext_items)
				{
					ext_item.WriteTo(svn_externals_builder, UseCompatibleFormat);
					svn_externals_builder.AppendLine();
				}

				using (SvnClient client = new SvnClient())
				{
					if (!client.SetProperty(target_path, "svn:externals", svn_externals_builder.ToString()))
					{
						Log.LogError("Could not set svn:externals property on target path: {0}.\n\nExternals:\n{1}", target_path, svn_externals.ToString());
					}
				}
			}
			catch (Exception e)
			{
				Log.LogErrorFromException(e);
				success = false;
			}

			return success;
		}

		private bool ShouldUpdateExternal(string target_name)
		{
			bool should_include = true; // default if no include/exclude targets provided

			if (IncludeTargets != null && IncludeTargets.Length > 0)
			{
				should_include = false; // don't include the task unless it's one of those explicitly included

				foreach (string target in IncludeTargets)
				{
					if (target.Equals(target_name, StringComparison.InvariantCultureIgnoreCase))
					{
						// found it
						should_include = true;
						break;
					}
				}
			}
			else if (ExcludeTargets != null && ExcludeTargets.Length > 0)
			{
				foreach (string target in ExcludeTargets)
				{
					if (target.Equals(target_name, StringComparison.InvariantCultureIgnoreCase))
					{
						// found it
						should_include = false;
						break;
					}
				}
			}

			return should_include;
		}

		private ITaskItem FindExternalTaskItem(string target_name)
		{
			ITaskItem task_item = null;
			foreach (ITaskItem item in m_externals)
			{
				if (item.ItemSpec.Equals(target_name, StringComparison.InvariantCultureIgnoreCase))
				{
					task_item = item;
					break;
				}
			}

			return task_item;
		}

		private SvnExternalItem CreateExternal(ITaskItem new_task_item)
		{
			if (new_task_item == null)
				return null;

			SvnRevision operative_revision = CreateRevision(new_task_item.GetMetadata("OperativeRevision"));
			SvnRevision peg_revision = CreateRevision(new_task_item.GetMetadata("PegRevision"));

			SvnExternalItem new_item = new SvnExternalItem(new_task_item.ItemSpec, new_task_item.GetMetadata("Reference"), operative_revision, peg_revision);

			return new_item;
		}

		private SvnRevision CreateRevision(string revision_text)
		{
			SvnRevision revision = null;
			int revision_number;

			if (!string.IsNullOrEmpty(revision_text))
			{
				if (revision_text.Equals("HEAD", StringComparison.InvariantCultureIgnoreCase))
				{
					revision = SvnRevision.Head;
				}
				else if (Int32.TryParse(revision_text, out revision_number))
				{
					revision = new SvnRevision(revision_number);
				}
				else
				{
					Log.LogWarning("Could not interpret revision identifier '{0}'.  Revision ignored.", revision_text);
				}
			}

			return revision;
		}
	}
}
